Aller au contenu

Versionning with GIT

Before starting...

Objectives

When you work with a computer, as a team or alone, you often need to save your progress, go back to a previous state of your work, allow for collaborators to contribute. This is what git is used for.

Using git is very simple, it does not require any complicated install or setup. This is why, even for a simple student labwork, handling your project directory with git is a good and lightweight idea.

This tutorial addresses the use of git in a specific way, which is very common. Understanding git from this specific usecase is a good entry point. Afterwards (not now...) you can imporove your knowledge by

The usecase is the following:

  1. I need a clever and safe disk space to work on a project. This disk space can be shared with others, actions on files can be undone, the successive stages of my work are versionned (i.e. I can go back to some previous state of the disk space), etc.
  2. I create a "git repository" at a remote party where the clever and safe disk space associated to my project is available.
  3. I mirror this disk space on a local disk, on my working machine. This is called "cloning" the remote git repository. The idea is to keep the two disk spaces synchronized: if I modify my local disk space, I have to make the remote one similar (we say "push" for this), and if somebody else has modified the remote disk space (by pushing), I have to bring these modifications to my local disk space (we say "pull" for this).
  4. If several people have cloned my project repository, they may push modifications that could create conflicts with what I am currently doing on my local disk. If I push them, or if I pull, these conflicts are detected... and git provides a nice way to handle this. Sharing work on the same files is very convenient with git.

How is your repository organized

In your disk space for your project, on your machine, you will have a subdirectory named .git/. It is automatically handled by the git commands you will run. Do not remove this subdirectory, it would remove all your git features for your project disk space.

The main Git elements are :

  • A file database. It is a directory (quite hidden to the user since it is within the .git directory) where all the versions of the files managed by git are stored. A specific version of some specific file that is stored is called a "blob" (Binary Large OBject). git does not manage "delta"s, i.e. it does not store an original file and then the successive modifications (i.e. deltas) that occurred to it. It stores a full copy of the first version of the file (one blob), a full copy of the second version (another blob corresponding to the same file), etc.

  • A working directory. This is the directory of your project. Git puts here the appropriate version of the files that it manages when, for example, you ask it to retrieve some specific version of your project. This is what the user sees, so git modifies the files in the working directory when you ask it to see something else.

  • A stage/index. Let us call it index. This contains the "current" version of all the files that git handles. This is not the working directory, since a file in the working directory may have been modified compared to the version that is stored in the index. The index can be viewed as the last snapshot of your project that git knows, under construction.

  • The history of commits. Commits are snapshots of your project (one commit is very similar to what the index is). They are organized as a tree, as detailed further. This tree organization makes git able to organize the snapshots as an history of your project life, allowing several "stories" to be followed in parallel, as explained further.

The main element of git is a "commit". It can be viewed as a snapshot of your entire project. This snapshot is indeed the list of the blob references corresponding to the files (at the right version) that belong to the picture. The index (or staging area) can be viewed as the current snapshot, under construction. We can progressively add elements to the index and, at some point, create a commit from the index; git then registers the snapshot and assigns it a label.

The files in the working directory can have different status:

  • untracked : No version of them is actually present in the index, i.e. git does not take care of them.

  • tracked : Git knows about them, i.e. the index contains a version of the file. A tracked file can itself have one of the following status:

    • unmodified : the file in the working directory is the same as in the index

    • modified : the file in the working directory is different from the index

    • staged : modifications of the file have been added to the snapshot under construction

Note that a file can be both modified and staged if, at some points, we decided to store a version of the file in the index and the file was later modified without being again added to the index.

The different status of a file. Image from progit2 available in the Pro Git book v2

Getting started

Having an account

Ok, first you need to have an account at some places where a git server can provide you with remote disk space. There are some "free" ones, as github.com. CentraleSupélec students can use gitlab-student, which is sovereign.

Use a ssh key for interactions with the server

Using git makes a lot of interactions from your computer with the remote git server. Typing your login and password for each of them is exhausting. To avoid this, register a ssh key on your git account.

Ask for help for this stage ! Using git whithout having a ssh-key available is extremely annoying.

Create your repository

The web interface of your git server provides you with the possibility to create a project. Here, this is illustrated on CentraleSupélec gitlab-student.centralesupelec.fr server. Create an empty project, let us call this project "gabuzo" for this tutorial (choose the name you want for yours).

Click the "new project" button

Then choose a project template... "Blank project" is enough for now.

Then you have to tell the project name (slug), set the visibility level (keep it private for now). You can toggle-out the "Initialize with a readme" box.

click to the "create project" button

Then you are launched to your project page. Find the "url" of it. On gitlab, this is the "code" button. The url we need is the "clone with ssh" one.

get the project url

For the gabuzo project on this illustration, the ssh-accessible url is git@gitlab-student.centralesupelec.fr:frezza/gabuzo.git... but, once again, replace this URL for the one you get.

This ssh-accessible url is denoted by <REMOTE_URL> in the following... so do not copy paste commands where <REMOTE_URL> appears, replace <REMOTE_URL> by something like git@gitlab-student.centralesupelec.fr:xxxx/gabuzo.git.

Clone your repository

Now, you are ready to have a disk space on your computer that is sychronized with the (empty for now) git repository you have just created.

First, the git command has to be available on your computer, make the required installations (sudo apt install git on Ubuntu, for example).

Then, at very first usage of git commands, you may be asked to define your name, e-mail... git suggests commands to do so, apply them first, and then retry.

Ok, let us clone. Go (cd <somewhere>) in a directory where you want your disk space to be created, and "clone" your project like this:

mylogin@mymachine:~$ git clone <REMOTE_URL>

It creates a gabuzo directory (or another one if you named your project differently). Let us go inside it.

mylogin@mymachine:~$ cd gabuzo

Let us check that it is actually a git directory:

mylogin@mymachine:~$ ls -al

You should see the .git subdirectory. Don't go into it, it is handled indirectly by the git commands you will type.

We can also check which is the remote repository we are synchronized with:

mylogin@mymachine:~$ git remote -v
origin  git@gitlab-student.centralesupelec.fr:xxxx/gabuzo.git (fetch)
origin  git@gitlab-student.centralesupelec.fr:xxxx/gabuzo.git (push)

Git can communicate with different servers, each communication channel is named, and can contain two directions: "fetch" to get data, "push" to send data. A channel is called a "remote", and here we have only one, named "origin". This is what you get by default when you clone a repository.

Start to work

Our first commits...

Edit (with a text file editor, as vscode... but not MSWord !) a Readme.md file, where you can write markdown code. You can add only a title line as # Welcome to the gabuzo project. Save the file (in the toplevel of your project directory... i.e. gabuzo/Readme.md).

This file is currently "untracked" (see section "How is your repository organized"). Git doesn't know it exists.

You can check this:

mylogin@mymachine:~$ git status
On branch main
Your branch is up to date with 'origin/main'.

...

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        Readme.md

It is not mandatory to check the status after each command but we will do that quite often here, for the sake of clarity.

Git considers that we have a current "snapshot" under construction, i.e. a kind of snapshot of your disk space that can be retrieved if needed. Let us add our Readme.md file to this under construction snapshot.

mylogin@mymachine:~$ git add Readme.md
mylogin@mymachine:~$ git status
On branch main
Your branch is up to date with 'origin/main'.

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   Readme.md

The Readme.md file has passed from an "untracked" status to a "staged" status. It belongs to the current under construction snapshot. It is associated with a "blob".

For the moment, you have no traces of different configurations that your disk space has taken... indeed we are just starting from scratch. Let us check this (the --something flags are not mandatory but they clarify the output).

mylogin@mymachine:~$ git log --all --oneline --decorate --graph

Let us create our first history point, i.e. our first commit. It is nice to provide a message associated to the commit (i.e. a label, some conventions exist). It is a kind of comment, since commit identifiers are just a numerical code.

mylogin@mymachine:~$ git commit -am "ADD: Readme.md"

The -am meaning will be explained soon.

Retry the git log command, you will see that our history has registered a first "historical event", i.e. a first commit.

mylogin@mymachine:~$ git log --all --oneline --decorate --graph

This is what I got

* 0f357ca (HEAD -> main) ADD: Readme.md

Here, 0f357ca is the code, i.e. the unique identifier, of the commit. This is called "the SHA of the commit" (SHA = Secure Hash Algorithm). You recognize the comment we have added. Let us focus our attention on the (HEAD -> main) information... We will go back to this soon.

Nota: The real SHA (for this example) is 0f357ca7d2f669dda5aa8506218d0dabf124f44d, but you can provide only the beginning of it. For example

mylogin@mymachine:~$ git show 0f357ca

shows information about that commit. That command is super usefull to check the content of a commit, i.e. all the modifications that have been staged in that commit.

Let us represent this first commit like this (SHA are 0000, 0001, ... on the figure, not the SHA that you actually got).

Git handles several references to some commit.

The main one is a reference to the commit that is the "present time" of the history. It is the commit from which future modification will be made. This is called the HEAD. So we see here that our single commit 0f357ca is the HEAD.

Git can handle other references, among which the "branches" are very usefull. A branch is only a reference to some commit. By default, we have a branch called "main" (on some older git servers, called "master"). The output of git log says 0f357ca (HEAD -> main) because both the "main" branch and the HEAD refer to our single commit 0f357ca.

Let us see how many branches we have:

mylogin@mymachine:~$ git branch

You may see a single "main" branch, with a * in front of it. The * means that we are currently in that branch (useless here since we have only one branch... ok). We will see later what "being in a branch" actually means.

Ok, let us add some other lines in the Readme.md file (add some text, and save).

Once done, try :

mylogin@mymachine:~$ git status

Ignore the message about "origin/main" for now (our remote server is empty...), just be aware that git knows that our file is no more identical to the "blob" associated with the commit 0f357ca. The Readme.md file is now in the "modified" status.

In order to register the change, we have to create another commit. If we need to add a new file in this new commit, which is "untracked", a git add newfile would be required. Here, we want to add the new version of our file in the snapshot under construction. The git add Readme.md would do the job, making the new version of Readme.md be in the "staged" status. But usually, in the next snapshot, what you want is to add the new versions of all the currently tracked files, i.e. all the files in "unmodified", "modified" and "staged" state. This is what the a in -am does, the m meaning only "the next word in the command is the message".

The first time you want to track files, you have to add them explicitly. After that, only a git commit -am ... does the tracking. So for us:

mylogin@mymachine:~$ git commit -am "MODIF: Readme.md"
mylogin@mymachine:~$ git status

Except the error, we see that everyting is ok. Let us detail what has actually happened.

mylogin@mymachine:~$ git log --all --oneline --decorate --graph

I got

* 21c05bd (HEAD -> main) MODIF: Readme.md
* 0f357ca ADD: Readme.md

On top, we see our new commit, 21c05bd, just above the previous 0f357ca we already had. When we have added the new commit 21c05bd, git considers that it is a "child" of an existing one. Which one ? The current HEAD. So git created commit 21c05bd as a child of HEAD, i.e. a child of 0f357ca since HEAD had that value. After this, HEAD is now positionned to this new 21c05bd commit.

Before adding the new commit, HEAD was also similar to main. "Being in the main branch" actually means the HEAD and main both refer to the same commit. The branch "we are in" follows the shift of HEAD to the new commit.

Let us sum up:

  • The history evolves from successive snapshots, named "commits".
  • Evolution is made by adding a commit as the child of an existing one.
  • That existing "parent" commit is one of the branches. That branch is the one equals to the head.
  • When the commit is added, the concerned branch and the head shift to the new commit.

Branches and head may appear redundant. Indeed they are not. When you have several branches (not the case for us now), you can place the head to the one from which you want your history to progress. This is called "swiching to another branch". When you switch to another branch (or more generally to another commit), the content of your directory will be changed to match what was present at that commit. This is an important use case of GIT. Usually, we develop a new feature on a branch. It happens we need to fix code in the main branch. Git allows to switch the content of your working directory to that of the main branch, you fix the code, commit and push the change, and then can switch back to your feature branch.

You can also set the head to be a commit which is not a branch... in this case, the head is said "detached". We will not use that, but if you see errors related to "detached head", it means thot what you want to do requires the head to be positionned at one of the branches.

Save your work at server side

At server side, the disk space associated to your project is still empty. The git commands you have just performed on your computer are not automatically transmitted to the server.

We said that the local disk space and the remote one are synchronized. This is not really true, since they become the same only when you decide to make them similar.

Let us do it:

mylogin@mymachine:~$ git push

And that's it. try again

mylogin@mymachine:~$ git log --all --oneline --decorate --graph

There is a branch at server side, named origin/main that has been created (origin is the name of our "remote"). You can also see the files on the web interface of your project at server side.

Edit a new dummy.txt file, text file with few words in it, and save it. This can be done by

mylogin@mymachine:~$ echo "Hello, I am a dummy file !" > dummy.txt

It is "untracked". Let us track it :

mylogin@mymachine:~$ git add dummy.txt
mylogin@mymachine:~$ git status

It is "staged" now.

Let us modify Readme.md again, and save it. It becomes "modified", so it is not "staged" yet... but commit -am ... will make it "staged" before creating the commit, so no need to git add the modified file, as previoulsy explained.

mylogin@mymachine:~$ git status

Then, let us make a new commit

mylogin@mymachine:~$ git commit -am "ADD: dummy"

Now, try

mylogin@mymachine:~$ git log --all --oneline --decorate --graph

You will see that the HEAD and the local main branch have moved to this new commit, but origin/main is still associated to the previous commit. As far as we know at client side, this previous commit is the one associated to the main branch at server side.

To get server up to date with our modifications:

mylogin@mymachine:~$ git push
mylogin@mymachine:~$ git log --all --oneline --decorate --graph

Using what we have seen so far, you are ready to work alone, using git to save your work. You can retrieve past versions of your file, from past commit (we did not detail how for now, wait for the end of that tutorial). At least, do this simple git management for working safely.

Even if you work alone, having several branches is a good way to organize you work. This is addressed next. Branches are also very convenient for collaboration, as addressed at the end of this tutorial.

Using multiple branches

In many situations, the history of a project is not "linear", i.e. registering a succession of commits, organized as a chain, does not reflect how the work is done. The typical case is when you have something quite finalized but you want to refactor some part of it. Refactoring takes several steps, so the refactoring process needs to be saved into a succession of commits, not only one. During the production of these commits, the project, that was finalized, gets polluted by some "at work" files, since the refactoring takes time and need several saved "snapshots" before leading to the next finalized version : we would like to keep the clean almost finalized project available and perform the refactoring aside.

Having multiple branches solves this kind of issues. From the current finalized status of your project, i.e. the from commit where both HEAD and main branch are positionned, we create a new refactoring branch. It is a third variable (or the fourth if we take origin/main into account) storing the SHA of the current commit. Then, we ask git to make HEAD and refactoring follow next commits, and no more main, that will stay positionned to the current commit. Asking this is what we call "switching to the refactoring branch". Then we produce new commits for refactoring. Whille producing new commits, the HEAD and refactoring will progress in the commit tree. At the end, once refactoring is finished, we will conciliate the two "diverging" branches, a so called merge. We illustrate all these steps below.

Let us illustrate the use of branches with a fake project. Go at the toplevel of your project, and place the files hello_world.py and fake_printings.py there.

Let us track them, and make the 'finalized" commit.

mylogin@mymachine:~$ git add *.py
mylogin@mymachine:~$ git commit -am finalized
mylogin@mymachine:~$ git push
mylogin@mymachine:~$ git log --all --oneline --decorate --graph

As this fake project is supposed to be finalized, let us try the code (read the .py files, but do not modify them unless you are told).

mylogin@mymachine:~$ python hello_world.py 
You may have noticed that the running of the python script produces a __pycache__ directory, for internal python use. Do not track this directory !!! There is indeed no need to save anything in there, since it is generated automatically. This is the reason why we do not recommend commands like git add *, that would make git track everything. Instead, add only the files you need, one by one, and next git commit -am ... commands will handle their savings naturally.

Working on a new branch

Now let us create the new branch refactoring. As the log shows, it is only a new reference to our last commit (where HEAD is positionned to).

mylogin@mymachine:~$ git branch refactoring
mylogin@mymachine:~$ git log --all --oneline --decorate --graph

Note that the log displays HEAD -> main.

You can see which branches we now have

mylogin@mymachine:~$ git branch
And we see that the current branch is main (it has a * before its name). The current branch is the one that will advance while commiting. While refactoring, we want these to be tracked with the refactoring variable, not main. Let us "switch" to the newly created branch (did you notice that the completion is available for git commands? I hope you did not type all the 'r', 'e', ..., 'i', 'n', 'g' keys)

mylogin@mymachine:~$ git switch refactoring
mylogin@mymachine:~$ git log --all --oneline --decorate --graph
Note that the log displays HEAD -> refactoring.

Ok, our refactoring job consists of changing the function definition in the file fake_printings.py. To do so, edit the file and apply what is referred to as CHANGE no 1 in this file (and only this). Save the file... and close the file editor.

Let us try it to run the python script (it is still buggy, your teachers did it on purpose...)

mylogin@mymachine:~$ python hello_world.py 
It was a great developper job, we need to save it as a commit.

mylogin@mymachine:~$ git commit -am "adding a frame"
mylogin@mymachine:~$ git log --all --oneline --decorate --graph

As you can see, HEAD is no more on the finalized commit, where the main branch still is, it tracks the refactoring branch which is the current branch. Let us save this work at server side. You will get an error the first time, but git tells you what to do (at that first time only). Try it:

mylogin@mymachine:~$ git push
The error suggest that we have to type the following for this first push on our new branch.

mylogin@mymachine:~$ git push --set-upstream origin refactoring

Switching an a branch for quick-fixing

As you have seen, the printing of the python script is not perfect, more work has to be done for having a new finalized version... but let us suppose, for the sake of the story of this GIT lab, that other users (they have cloned your git repository on their computer) complain about a bug in the finalized version of the main branch. They would like few line breaks before the printing.

You have to interrupt your refactoring, go back to the finalized version at the main branch, and fix the bug. Let us do this.

mylogin@mymachine:~$ git switch main

You can check with git branch that you are (your current branch is) back on main. Re-edit the fake_printings.py file just for reading its content: Oh ! Did you notice that the content of fake_printings.py has changed ?! Indeed, git has taken its version from the blob associated to the "finalized" commit and has restored the file as it was. No worries, your refactoring in this file is not lost, it is stored in another blob, associated to the commit "adding a frame". Close the editor.

Edit the hello_world.py file, apply CHANGE no 2 to fix the bug. We save the fix in a new commit... associated to the main branch.

mylogin@mymachine:~$ git commit -am "BUGFIX: 2 line breaks"
mylogin@mymachine:~$ git push
mylogin@mymachine:~$ git log --all --oneline --decorate --graph

The fix of the super critical bug is now done, we can go back to our refactoring work.

Merging a branch

mylogin@mymachine:~$ git switch refactoring

The file fake_printings.py contains the changes we have made (hopefully !) but hello_world.py has not the bug fix we have just done. Let us ignore this for now.

We have to finish the work, by adjusting the framing of the text. Apply CHANGE no 3 in fake_printings.py to do so.

Let us test:

mylogin@mymachine:~$ python hello_world.py 
And commit the change

mylogin@mymachine:~$ git commit -am "BUGFIX: frame adjusted"
mylogin@mymachine:~$ git push
mylogin@mymachine:~$ git log --all --oneline --decorate --graph

Ok, the refactoring job is done, we can make it available for the users relying on what happens on the main branch. To do so, we have to switch on main, and ask for a new commit that integrates the current main (where the HEAD is after the switch) with the commit referred by refactoring. This is called "merging refactoring into main". For the sake of clarity, we log after each step... but it is not needed.

Nota: The git merge command opens an inline text editor (nano or vi by default on Linux but this can be changed by setting the core.editor git variable). It contains the comment of the new commit created by this operation. Just save to proceed (CTRL-X with nano, or :wq with vi).

mylogin@mymachine:~$ git switch main
mylogin@mymachine:~$ git log --all --oneline --decorate --graph

mylogin@mymachine:~$ git merge refactoring
mylogin@mymachine:~$ git log --all --oneline --decorate --graph

mylogin@mymachine:~$ git push

You may have noticed that the git merge command has made a new commit, "Merge branch 'refactoring'", that has two parents, the commit where the refactoring branch is positionned to and the commit where the HEAD (and main) was positionned to. That's it ! The main branch contains all the bug fixes, in both files.

Here, things went very easily since merging only consists in gathering the latest versions of the files in each branch. This is why it is a good idea to spread your code in many elementary files (this is natural for programming... but consider this for your latex projects as well, using \input{myfilme.tex}).

When merge conflicts

Sometimes, merges conflict. Let us produce such a conflict in order to see how this can be solved.

Let us go in main, and modify hello-world.py so that it prints the message in French (Bonjour le monde). To do so (we are already in the main branch, so next switch does nothing)...

mylogin@mymachine:~$ git switch main
Edit the file, change the message, and save. Then commit and push.

mylogin@mymachine:~$ git commit -am "MODIF: French"
mylogin@mymachine:~$ git push
mylogin@mymachine:~$ git log --all --oneline --decorate --graph

Let us do the same on the refactoring branch (yes, we can still use it after the merge, it is not automatically deleted), but saying the message in German (Hallo Welt).

mylogin@mymachine:~$ git switch refactoring

Edit the file, change the message, and save. Then commit and push.

mylogin@mymachine:~$ git commit -am "MODIF: German"
mylogin@mymachine:~$ git push
mylogin@mymachine:~$ git log --all --oneline --decorate --graph

Let us go back in main...

mylogin@mymachine:~$ git switch main

... and merge.

mylogin@mymachine:~$ git switch merge refactoring

Git notifies a conflict... Indeed, should we keep German or French ? Git does his best to merge diverging code bases. When the changes are in different files, it is straightforward to merge the changes. However, there is no magic. If it happens your changes are on the same line but with different contents, it is your responsibility to decide which change to keep. Still, Git is sufficiently smart to tag the parts of the files for which conflicts must be manually solved.

Look at

mylogin@mymachine:~$ git log --all --oneline --decorate --graph

Now merging commit has been created ! Everything is ready in the stage, but the commit has not been made. You'll have to commit by yourself. You could do it right now, but you have to modify the files where the conflicts are. Edit hello-world.py... git has modified the file to highlight the conflict, by adding some kinds of comments (<<<< and >>>>). Take your decision, clean up the file and save.

Then you can commit.

mylogin@mymachine:~$ git commit -am "French chosen"
mylogin@mymachine:~$ git push
mylogin@mymachine:~$ git log --all --oneline --decorate --graph

Collaboration

Main principle

Working with others as a team is one nice feature offered by git. What we have seen previously is enough for that.

Indeed, from the leb interface of your project (either on github.com or any gitlab instance), you can add other "members" of your project, giving them a role (choose the "maintainer" role for a workmate). They will be able to access the interface and clone the project on their computer, as you did.

They can, on their own side, make commits, and push them on the server. When they do so, the history of commits progresses on their computer and on the server, but not on your computer. In other words, the history of commits on your computer becomes outdated.

To get on your computer the updated version of the current branch your are in, the command is pull. But be carefull, invoke that command only when all your tracked files have been placed into a commit (you may also git stash to revert your changes and put them on a stack, temporarily). Otherwise the pull command warns you that some files can be lost because of the pulling action.

So to sum, in order to be up to date in branch

mylogin@mymachine:~$ git commit -am "..."
mylogin@mymachine:~$ git pull
And that's it. When you try to git push while the server has received some commits since your last interaction with it, i.e. you want to push but are late, git refuses the push and asks you to pull first.

In this case the pulling may generate file conflits with the commits you have added since your last interaction. Indeed, from your last git push, you may have added commits, as did your workmate. But s/he has put his/her commits on the server first.

Solving conflicts in this case is very close to what we did for branches (follow git instructions).

To avoid this, the best practise is to start your work by a first git pull, and then push every commit you make once it is created, do not wait to build a long chain of commits before pushing it to the server.

Using branches

You already know how branches work, you can push/pull any branch you are in. Your workmates do the same.

Students often create "their" branch, named with their pseudo. So each member of the project has its own branch, where they merge main to get the latest version of the project, work, and merge their branch back into main when the job is done. We do not recommend that.

Consider a branch as some kind of problem to be solved. In a software projet, a branch could be "adding a graphical interface", and many collaborators can work on it. The refactoring branch in our fake project has this flavour. Adding new features, refactoring some parts, etc.. are good reasons for creating a branch.

Retrieving a deleted file or content

Remember that git is designed to keep track of changes. This is useful in several situations, for example when you removed a critical file by accident, or some text in a file, and that you realize that mistake later on. It also happens that you are not satisfied by a change you applied and want to revert it. All these use cases can be handled with git.

To illustrate the retrieval of a deleted file, let us delete hello_world.py, add a new file, and commit.

mylogin@mymachine:~$ git switch main
mylogin@mymachine:~$ rm hello_world.py
mylogin@mymachine:~$ echo "I am a new file" > nice-file.txt
mylogin@mymachine:~$ git add nice-file.txt
mylogin@mymachine:~$ git commit -am "Adding nice file"
mylogin@mymachine:~$ git push

Here, we removed the file from the workspace, using rm .., to illustrate this situation. We could also add the removal of the file as one of the blobs of the next commit, in which case we would have used git rm .... But the idea here is to consider that the deletion has been made accidentally (git can also handle the case a file has been deleted by a commit). Actually, the -am in the git commit is adding both the creation of the nice-file.txt and the deletion of hello_world.py in the commit. Hence, although we did not call git rm .. but only rm .., the subsequent git commit -am will create a commit with the deletion of hello_world.py.

Let us proceed simulating some further modifications of our repository and create another file, and add it.

mylogin@mymachine:~$ echo "Ok... write what you want" > another-file.txt
mylogin@mymachine:~$ git add another-file.txt
mylogin@mymachine:~$ git commit -am "Another file added"
mylogin@mymachine:~$ git push

Then, I realize that hello_world.py is missing ! Oh no, my work is lost ! This can be very dramatic if this file is the state of the art of your PhD manuscript...

Do not panic. Git has saved it in one blob. Nota: It is saved at least twice, since your machine as well as the server have it. Moreover, git servers belong to institutions that have professionnal (and thus quite safe) disk spaces.

First, let us identify which is the first commit where the file is not present, in order to see who made the mistake, and when.

mylogin@mymachine:~$ git log --max-count=1 --diff-filter=D -- hello_world.py

Nota: each commit has a diff status for its files, telling the difference with its parent commit (Added, Modified, Deleted, ...). Here the filter extracts the latest commit that has D status for the file hello_world.py. You can see the SHA of the commit, who did it, and when.

Let us denote the SHA as 1234567890 (but of course, you got something else).

The last commit containing the hello_world.py file is the parent of the one we just have identified. Parent is denoted by ^ in git notations. So to get the SHA of the last commit having our file:

mylogin@mymachine:~$ git rev-parse 1234567890^
or just, by truncating the SHA

mylogin@mymachine:~$ git rev-parse 1234^
You get the SHA of the parent of 1234567890, let us suppose here that it is 0987654321 (but of course, once again, you got something else).

You can restore the lost file in your working directory from that commit and then add it:

mylogin@mymachine:~$ git restore --source=09876 -- hello_world.py
mylogin@mymachine:~$ git add hello_world.py
mylogin@mymachine:~$ git commit -am "Restoring hello_world.py"
mylogin@mymachine:~$ git push

Other features and pieces of advice

Do not handle big files

Big files are associated with big blobs. If the file is a 200Mo video, that you modify and track for 10 commits, you will have 10 200Mo-sized blobs for each version of the video.

Also, be careful, because as git handles an history, even if you delete a large file you accidentaly added, it is still saved in the history because that's how git is designed. Hence, your git repository size will still be affected by that large file you once added, although it is yet "deleted".

Moving a file

You can move a tracked file, i.e. changing its name and keep on tracking it. This is the git mv SRC DST command, that behaves as the mv bash command.

Where can I use git commands ?

As previously mentionned, the relation between your local directory and the one at server side, as well as all git tracking features, is handled by the data stored in the .git/ subdirectory at the toplevel of your project directory. You can go (cd <directory>) in sub-directories, and invoke git commands on the terminal, this will work. Indeed, git searches the parent directories of the current one until it finds the one hosting a .git/ subdirectory.

This implies that in a directory hierarchy like /home/me/.../A/.../B/.../here/, if both A and B have .git/ subdirectory, working in directory here is working on the git-managed directory B (B shadows A). In other words, having a git-managed repository (B) as subdirectory of a git-managed repository (A) is weird, and should not happen.

Nevertheless, git has the concept of "git submodules" for that. This can be very convenient, but this is out pf the scope of this tutorial.

Versionning empty directories

Only files are versionned by git, so you cannot force your repository at server side to have an empty directory, for some further use. The trick in this case is to create an empty file, named .gitkeep (.* files are hidden files, they are not displayed by default by ls or GUI file explorers), and track it. The name .gitkeep is what people usually use, any empty file does the trick. (Nota: touch <file> sets the current time to the file... or creates if it as empty if it does not exist yet).

mylogin@mymachine:~$ mkdir MyEmptyDir
mylogin@mymachine:~$ touch MyEmptyDir/.gitkeep
mylogin@mymachine:~$ git add MyEmptyDir/.gitkeep
mylogin@mymachine:~$ git commit -am "ADD: MyEmptyDir"
mylogin@mymachine:~$ git push

Ignoring files

You can add a file named .gitignore in some subdirectory. It tells git which file you do not want to track (there is a small syntax for that). This is usefull if you want to type git add * or git add some-dir/, but you do not want to track files like *.o, *.backup, ...

We have encouraged the expicit adding of each new file you want to track, and then the use of git commit -am ... to track them all. This way of using git will prevent accidentally adding undesired files.

However, a .gitignore can also be very usefull to get a readable git status output. Indeed, the git status command outputs the state of the files (modified, removed, untracked) only of files that are not ignored.

For example, if you host a latex document in a git repository, the following .gitignore will prevent displaying the status of latex related temporary files :

*.aux
*.bbl
*.blg
*.ilg
*.out
*.toc
*.ind
*.flg 
*.fls
*.glo
*.log
*.idx
*.maf 
*.mtc*
*eps-converted-to*
*fdb_latexmk

You can also find templates of gitignore files for Python , C++, etc..., see for example https://github.com/github/gitignore/tree/main.